这个笔记是根据慕课网上一门叫做《Java秒杀系统方案优化 高性能高并发实战》学习整理的笔记。学习应对高并发场景如何设计接口,以及后端架构如何优化等知识,觉得还是学到一些东西的,就分享在这里。第一篇主要是完成用户登陆模块,借此搭建起一个基本的系统。

目标:初步实现用户登录功能.

1. user表结构

1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE `NewTable` (
`id` bigint NOT NULL COMMENT '手机号码' ,
`nickname` varchar(255) NOT NULL COMMENT '登录名' ,
`password` varchar(32) NOT NULL COMMENT 'md5(md5(pass+固定salt)+salt)' ,
`salt` varchar(10) NOT NULL COMMENT '盐值' ,
`head` varchar(128) NOT NULL COMMENT '头像' ,
`register_date` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '注册时间' ,
`last_login_date` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '上次登录时间' ,
`login_count` int(11) NOT NULL DEFAULT 0 COMMENT '登录次数' ,
PRIMARY KEY (`id`)
)
;

两次MD5

  • 用户端:PASS=MD5(明文+固定salt):防止明文密码在网络传输时被截取
  • 服务端:PASS=MD5(用户输入+随机salt):防止数据库被盗

2. 代码逻辑

2.1 前端处理

这里在前端对密码进行了一次md5加密。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<script>
function login(){
$("#loginForm").validate({
submitHandler:function(form){
doLogin();
}
});
}
function doLogin(){
g_showLoading();

var inputPass = $("#password").val();
var salt = g_passsword_salt;
var str = ""+salt.charAt(0)+salt.charAt(2) + inputPass +salt.charAt(5) + salt.charAt(4);
var password = md5(str);

$.ajax({
url: "/login/do_login",
type: "POST",
data:{
mobile:$("#mobile").val(),
password: password
},
success:function(data){
layer.closeAll();
if(data.code == 0){
layer.msg("成功");
window.location.href="/goods/to_list";
}else{
layer.msg(data.msg);
}
},
error:function(){
layer.closeAll();
}
});
}
</script>

这里前端的渲染模板用的是thymeleaf

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

后端如何优雅地处理呢?

2.2 定义一个vo来接收前端数据

1
2
3
4
5
@Data
public class LoginVo {
private String mobile;
private String password;
}

2.3 数据校验

我们可以用jsr303来进行校验,而不需要写很多代码来实现。

1
2
3
4
5
6
7
8
9
10
@Data
public class LoginVo {
@NotNull
@IsMobile
private String mobile;

@NotNull
@Length(min=32)
private String password;
}

这里需要依赖:

1
2
3
4
5
<!--jsr303-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

对于其中的判断手机号码是否存在,我们需要自己来实现一下这个注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {IsMobileValidator.class })
public @interface IsMobile {

boolean required() default true;

String message() default "手机号码格式错误";

Class<?>[] groups() default { };

Class<? extends Payload>[] payload() default { };
}

这个注解的功能是由IsMobileValidator.class来完成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class IsMobileValidator implements ConstraintValidator<IsMobile, String> {

private boolean required = false;

public void initialize(IsMobile constraintAnnotation) {
required = constraintAnnotation.required();
}

public boolean isValid(String value, ConstraintValidatorContext context) {
if(required) {
return ValidatorUtil.isMobile(value);
}else {
if(StringUtils.isEmpty(value)) {
return true;
}else {
return ValidatorUtil.isMobile(value);
}
}
}

}

其中,ValidatorUtil.isMobile(value)是真正用来验证手机格式的:

1
2
3
4
5
6
7
8
9
10
11
12
public class ValidatorUtil {

private static final Pattern mobile_pattern = Pattern.compile("1\\d{10}");

public static boolean isMobile(String src) {
if(StringUtils.isEmpty(src)) {
return false;
}
Matcher m = mobile_pattern.matcher(src);
return m.matches();
}
}

这样,我们就可以实现对前端传来的参数进行校验了:@Valid LoginVo loginVo

1
2
3
4
5
6
@RequestMapping("/do_login")
@ResponseBody
public Result<Boolean> doLogin(@Valid LoginVo loginVo){
userService.login(loginVo);
return Result.success(true);
}

2.4 全局异常

当校验参数时,这个参数时有问题时,我们需要一个全局异常来进行处理,将异常信息以合适的形式传给前端:

GlobalException:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class GlobalException extends RuntimeException{

private static final long serialVersionUID = 1L;

private CodeMsg cm;

public GlobalException(CodeMsg cm) {
super(cm.toString());
this.cm = cm;
}

public CodeMsg getCm() {
return cm;
}
}

下面就是需要对异常进行拦截和处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {
@ExceptionHandler(value=Exception.class)
public Result<String> exceptionHandler(HttpServletRequest request, Exception e){
e.printStackTrace();
if(e instanceof GlobalException) {//自定义的全局异常
GlobalException ex = (GlobalException)e;
return Result.error(ex.getCm());
}else if(e instanceof BindException) {//数据参数校验的异常
BindException ex = (BindException)e;
List<ObjectError> errors = ex.getAllErrors();
ObjectError error = errors.get(0);
String msg = error.getDefaultMessage();
return Result.error(CodeMsg.BIND_ERROR.fillArgs(msg));
}else {
return Result.error(CodeMsg.SERVER_ERROR);
}
}
}

2.5 返回结果封装类

我们给前端返回的结果要有一个统一的格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@Data
public class Result<T> {

private int code;
private String msg;
private T data;

/**
* 成功时候的调用
* */
public static <T> Result<T> success(T data){
return new Result<T>(data);
}

/**
* 失败时候的调用
* */
public static <T> Result<T> error(CodeMsg codeMsg){
return new Result<T>(codeMsg);
}

private Result(T data) {
this.data = data;
}

private Result(int code, String msg) {
this.code = code;
this.msg = msg;
}

private Result(CodeMsg codeMsg) {
if(codeMsg != null) {
this.code = codeMsg.getCode();
this.msg = codeMsg.getMsg();
}
}

}

2.6 异常信息分类

因为会产生各种异常,为了方便出现问题时很快定位到异常的类型,我们需要对异常的类型进行统一的管理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class CodeMsg {

private int code;
private String msg;

//通用的错误码
public static CodeMsg SUCCESS = new CodeMsg(0, "success");
public static CodeMsg SERVER_ERROR = new CodeMsg(500100, "服务端异常");
public static CodeMsg BIND_ERROR = new CodeMsg(500101, "参数校验异常:%s");
//登录模块 5002XX
public static CodeMsg SESSION_ERROR = new CodeMsg(500210, "Session不存在或者已经失效");
public static CodeMsg PASSWORD_EMPTY = new CodeMsg(500211, "登录密码不能为空");
public static CodeMsg MOBILE_EMPTY = new CodeMsg(500212, "手机号不能为空");
public static CodeMsg MOBILE_ERROR = new CodeMsg(500213, "手机号格式错误");
public static CodeMsg MOBILE_NOT_EXIST = new CodeMsg(500214, "手机号不存在");
public static CodeMsg PASSWORD_ERROR = new CodeMsg(500215, "密码错误");

//商品模块 5003XX

//订单模块 5004XX

//秒杀模块 5005XX


public CodeMsg fillArgs(Object... args) {
int code = this.code;
String message = String.format(this.msg, args);
return new CodeMsg(code, message);
}

}

2.7 login登录逻辑

手机号码不存在或者密码不匹配,直接抛出全局异常异常,这个异常信息会被拦截,最后处理成相应的统一的格式进行返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public boolean login(LoginVo loginVo) {
if (loginVo == null)
throw new GlobalException(CodeMsg.SERVER_ERROR);
String mobile = loginVo.getMobile();
String password = loginVo.getPassword();

//判断手机号码是否存在
MiaoshaUser user = getById(Long.parseLong(mobile));
if(user == null){
throw new GlobalException(CodeMsg.MOBILE_NOT_EXIST);
}

//验证密码是否匹配
String dbPass = user.getPassword();
String dbSalt = user.getSalt();
if(!MD5Util.formPassToDBPass(password,dbSalt).equals(dbPass)){
throw new GlobalException(CodeMsg.PASSWORD_ERROR);
}

return true;
}

这里需要一个MD5的工具类,不贴了,但是注意要添加依赖:

1
2
3
4
5
6
7
8
9
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.1</version>
</dependency>